(* ::Package:: *)

(* ::Title:: *)
(*CocoaLink*)


(* ::Author:: *)
(*Rob Raguet-Schofield*)
(*Wolfram Research*)


(* ::Section:: *)
(*Initialization*)


BeginPackage["CocoaLink`"];


(* ::Section:: *)
(*Interface*)


(* ::Subsection:: *)
(*Usage Messages*)


(* ::Subsubsection:: *)
(*Class Functions*)


CocoaClass::usage = 
"CocoaClass[name_String] creates a CocoaObject expression representing the given cocoa class name";


CocoaClasses::usage = 
"CocoaClasses[] returns a list of all class names which are currently loaded by the Objective-C runtime";


CocoaLoadClass::usage = 
"CocoaLoadClass[name:((_String|_Symbol)..)] loads the given class and attempts to create a symbol of that name with the class object assigned to it";
SetAttributes[CocoaLoadClass, HoldAll];


(* ::Subsubsection:: *)
(*Object Functions*)


CocoaObject::usage = 
"CocoaObjectPattern is used to represent a cocoa object as an expression";


CocoaObjects::usage = 
"CocoaObjects[] returns a list of all allocated cocoa objects which have an expression representation";


CocoaObjectMethods::usage = 
"CocoaObjectMethods[obj:(CocoaObjectPattern..)] returns a list of all (well, most of) the method selectors the given object responds to";


CocoaAlloc::usage = 
"CocoaAlloc[classname_String, initmethod_String, { args___ }] is used to allocate an object of the given class, and call the given initialization method with the given arguments.  This is needed because you are not allowed to call memory management methods such as \"alloc\" directly.";


CocoaObjectRelease::usage = 
"CocoaObjectRelease[obj:(CocoaObjectPattern..)] releases an object once you are done with it";


CocoaObjectRefCount::usage =
"CocoaObjectRefCount[obj:(CocoaObjectPattern..)] is a debugging function to get the reference count of an object";


CocoaObjectClass::usage = 
"CocoaObjectClass[obj:(CocoaObjectPattern..)] returns the name of the class of the given object belongs to if it is an instance, or itself it is a class";


CocoaObjectSuperclass::usage =
"CocoaObjectSuperclass[obj:(CocoaObjectPattern..)] returns the name of the superclass of the given object instance or class";


CocoaObjectDescription::usage = 
"CocoaObjectDescription[obj:(CocoaObjectPattern..)] returns a textual description of a given object useful for debugging";


(* ::Subsubsection:: *)
(*Runtime Functions*)


CocoaSend::usage = 
"CocoaSend[obj:CocoaObjectPattern, selector_String, { args___ }] is a low level interface to send a message (method call) to an object";


CocoaBlock::usage = 
"CocoaBlock[_] executes the given code, releasing any cocoa objects which were created during execution (unless that CocoaObject is returned from the expression)";
SetAttributes[CocoaBlock, HoldAllComplete];


CocoaClear::usage = 
"CocoaClear[] releases all cocoa objects";


CocoaData::usage =
"CocoaData[_String] returns a NSData object containing the raw bytes of the given string.";


CocoaSignature::usage =
"CocoaSignature[sig_String] returns a NSMethodSignature described by sig.";

CocoaLoadNib::usage = 
"CocoaLoadNib[path_String, owner:(CocoaObjectPattern)] loads a nib file and returns a list of objects.  Owner is set as the file's owner.";

CocoaToExpression::usage = 
"CocoaToExpression[obj:(CocoaObjectPattern..)] attempts to convert the given cocoa object into an expression.  If the object cannot be represented as an expression, the CocoaObject will be returned.";


CocoaSelector::usage = 
"CocoaSelector[name:(_String|_Symbol)] converts between the string and symbol representations of a method selector name";
SetAttributes[CocoaSelector, HoldAll];


CocoaDelegate::usage =
"CocoaDelegate[expr_, {\"methodName\" -> methodImpl}] create a CocoaObject whose method are implemented in Mathematica code.";


CocoaRun::usage = 
"This doesn't work yet, don't use it!";


CocoaQuit::usage = 
"CocoaQuit[] ends a session started by CocoaRun[]";


(* ::Subsubsection:: *)
(*Debug Functions*)


CocoaObjectDebugNotebook::usage =
"CocoaObjectDebugNotebook[] creates a notebook useful for debugging";


CocoaObjectMethodsNotebook::usage =
"CocoaObjectMethodsNotebook[obj:CocoaObjectPattern] creates a notebook listing all (well, most of) the method selectors the given object responds to";


CocoaClassesNotebook::usage = 
"CocoaClassesNotebook[] creates a notebook listing all classes currently loaded in the Objective-C runtime";


(* ::Subsection::Closed:: *)
(*Error Messages*)


CocoaObject::inval="`1` is not a valid cocoa object";

CocoaObject::argx="Could not determine arguments from `1`";

CocoaObject::mem="You may not call memory management function `1`";

CocoaObject::sel="`1` does not respond to selector `2`";

CocoaObject::conv="Could not convert `1` to type `2`";

CocoaObject::expt="An exception (`1`) occurred: `2`";

CocoaClass::inval="`1` is not a valid class name";

CocoaClass::alloc="Instances of class `1` cannot be directly allocated";

CocoaSelector::inval="`1` is not a valid method name"


(* ::Section:: *)
(*Implementation*)


Begin["`Private`"];


(* ::Text:: *)
(**)


$Link = Install["CocoaLink"];


(* ::Subsection:: *)
(*Common*)


CocoaObjectInitialize[obj:(CocoaObjectPattern)] := (
	If[Context[obj] === "CocoaLink`Objects`",
		SetAttributes[obj, HoldAllComplete];
		obj[sel_Symbol[args___]] := CocoaObjectInitialize[CocoaSend[obj, CocoaSelector[sel], { args }]];
		obj[sel_String[args___]] := CocoaObjectInitialize[CocoaSend[obj, sel, { args }]];
		obj[sel_Symbol[args___][subargs___]] := CocoaObjectInitialize[CocoaSend[obj, CocoaSelector[sel], { args }]][subargs];
		obj[sel_String[args___][subargs___]] := CocoaObjectInitialize[CocoaSend[obj, sel, { args }]][subargs];
		With[{class = CocoaObjectClass[obj]},
			MakeBoxes[obj, fmt_] ^= InterpretationBox[RowBox[{"\[LeftGuillemet]",
				RowBox[{"CocoaObject", "[", class, "]"}],
				"\[RightGuillemet]"}], obj]
		];
	];
	obj
);


CocoaObjectInitialize[x_List] := CocoaObjectInitialize /@ x;


CocoaObjectInitialize[Rule[x_, y_]] :=
	Rule[CocoaObjectInitialize[x], CocoaObjectInitialize[y]];


CocoaObjectInitialize[x_] := x;


(*CocoaObject[obj_Integer][sel_Symbol[args___]] := CocoaSend[CocoaObject[obj], CocoaSelector[sel], { args }];*)
(*CocoaObject[obj_Integer][sel_String[args___]] := CocoaSend[CocoaObject[obj], sel, { args }];*)


(*CocoaObject[obj_Integer][sel_Symbol[args___][subargs___]] := CocoaSend[CocoaObject[obj], CocoaSelector[sel], { args }][subargs];*)
(*CocoaObject[obj_Integer][sel_String[args___][subargs___]] := CocoaSend[CocoaObject[obj], sel, { args }][subargs];*)


(* ::Subsection:: *)
(*Class Functions*)


CocoaClass[class_String] := CocoaObjectInitialize[cocoaClass[class]];


CocoaLoadOneClass[class_String] := Module[{ wasOn1, wasOn2, obj },
	{ wasOn1, wasOn2 } = (Head[#] =!= $Off &) /@ { Symbol::symname, Set::write };
	Off[Symbol::symname, Set::write];
	Clear[class];
	obj = CocoaClass[class];
	CocoaSet[Symbol[class], obj];
	If[wasOn1, On[Symbol::symname]];
	If[wasOn2, On[Set::write]];
	obj
];


CocoaLoadClass[classes:((_Symbol|_String)..)] := Module[{ cls },
	Clear[classes];
	cls = CocoaLoadOneClass[ToString[#]]& /@ { classes };
	If[Length[cls] === 1,
		cls[[1]],
		cls
	]
];


(* ::Subsection:: *)
(*Object Functions*)


CocoaObjects[] :=
	CocoaObjectInitialize[cocoaObjects[]];


CocoaAlloc[class_String] := CocoaAlloc[class, "init"];


CocoaAlloc[class_String, init_String, args___] :=
	CocoaObjectInitialize[cocoaAlloc[class, init, { args }]];


CocoaObjectClass[obj:(CocoaObjectPattern)] :=
	CocoaObjectInitialize[cocoaObjectClass[obj]];


CocoaObjectSuperclass[obj:(CocoaObjectPattern)] :=
	CocoaObjectInitialize[cocoaObjectSuperclass[obj]];


(* ::Subsection:: *)
(*Runtime Functions*)


CocoaSet[a_Symbol, b_] := Set[a, b]; (* non-HoldFirst Set function *)


CocoaBlock[e_] := Module[{ objs, res },
	objs = CocoaObjects[];
	res = e;
	CocoaRelease /@ DeleteCases[
		Complement[CocoaObjects[], objs],
		res
	];
	res
];


CocoaClear[] := (
	CocoaObjectRelease /@ CocoaObjects[];
	Remove["CocoaLink`Objects`*"];
);


CocoaData[data_String] :=
	CocoaObjectInitialize[cocoaData[data]];


CocoaSignature[sig_String] := CocoaBlock[
	CocoaClass["NSMethodSignature"]@"signatureWithObjCTypes:"[sig]
];


CocoaLoadNib[path_String, owner:(CocoaObjectPattern)] := CocoaBlock[
	Module[{url, nib, objects, table},
		(* load the nib from a file URL *)
		url = CocoaClass["NSURL"]@"fileURLWithPath:"[path];
		If[!CocoaObjectQ[url], Return[$Failed]];
		nib = CocoaAlloc["NSNib", "initWithContentsOfURL:", url];
		If[!CocoaObjectQ[nib], Return[$Failed]];
		
		(* create the mutable object array and name table *)
		objects = CocoaClass["NSMutableArray"]@"array"[];
		table = CocoaClass["NSMutableDictionary"]@"dictionary"[];
		table@"setValue:forKey:"[owner, "NSOwner"];
		table@"setValue:forKey:"[objects, "NSTopLevelObjects"];
		
		(* instantiate the nib *)
		If[!nib@"instantiateNibWithExternalNameTable:"[table], Return[$Failed]];
		CocoaToExpression[objects]
	]
];


CocoaToExpression[Null] := Null;


CocoaToExpression[obj:(CocoaObjectPattern)] :=
	CocoaObjectInitialize[cocoaToExpression[obj]];


CocoaSelector[sel_Symbol] := StringReplace[ToString[Unevaluated[sel]], {"$$" -> "_", "$" -> ":"}];
CocoaSelector[sel_String] := Module[{ wasOn1, wasOn2, sym },
	{ wasOn1, wasOn2 } = (Head[#] =!= $Off &) /@ { General::spell, General::spell1 };
	Off[General::spell, General::spell1];
	sym = Symbol@StringReplace[sel, { "_" -> "$$", ":" -> "$" }];
	If[wasOn1, On[General::spell]];
	If[wasOn2, On[General::spell1]];
	sym
];


CocoaDelegate[expr_, methods_List] :=
	CocoaObjectInitialize[cocoaDelegate[expr, methods]];


CocoaQuit[___] :=
	CocoaClass["NSApplication"]@"sharedApplication"[]@"stop:"[Null];


(* ::Subsection::Closed:: *)
(*Debugging methods*)


CocoaObjectDebugUpdateNotebook[nb_NotebookObject] := Module[{ cell },
	cell = Cell[BoxData[CocoaObjectDebugCell[]], 
		CellTags->"CocoaCell", Editable->False];
		
	If[nb =!= $Failed && NotebookFind[nb, "CocoaCell", All, CellTags] =!= $Failed,
		NotebookWrite[nb, cell, All],
		CellPrint[cell]
	];
];


CocoaObjectDebugCell[] := Module[{ objs = CocoaObjects[], table, klass },
	table = {
		ButtonBox["x",
			ButtonEvaluator->Automatic,
			ButtonFunction:>(CocoaObjectRelease[#]; CocoaObjectDebugUpdateNotebook[ButtonNotebook[]]),
			Active->True
		],
		ButtonBox[ToString[#],
			ButtonEvaluator->Automatic,
			ButtonFunction:>CocoaObjectMethodsNotebook[#],
			Active->True
		],
		ButtonBox[CocoaObjectClass[#],
			ButtonEvaluator->Automatic,
			ButtonFunction:>(CocoaObjectMethodsNotebook[CocoaClass[CocoaObjectClass[#]]]; CocoaObjectDebugUpdateNotebook[ButtonNotebook[]]),
			Active->True
		],
		(klass = CocoaObjectSuperclass[#]; If[klass =!= NULL,
			ButtonBox[klass,
				ButtonEvaluator->Automatic,
				ButtonFunction:>(CocoaObjectMethodsNotebook[CocoaClass[CocoaObjectSuperclass[#]]]; CocoaObjectDebugUpdateNotebook[ButtonNotebook[]]),
				Active->True
			],
			(*else*)
			"Null"
		]),
		CocoaObjectRefCount[#],
		CocoaObjectDescription[#]
	} & /@ objs;
	table = Insert[table, {
		"",
		StyleBox["Object", FontWeight -> "Bold", FontVariations -> {"Underline" -> True}],
		StyleBox["Class", FontWeight -> "Bold", FontVariations -> {"Underline" -> True}],
		StyleBox["Super", FontWeight -> "Bold", FontVariations -> {"Underline" -> True}],
		StyleBox["Ref", FontWeight -> "Bold", FontVariations -> {"Underline" -> True}],
		StyleBox["Description", FontWeight -> "Bold", FontVariations -> {"Underline" -> True}]
	}, 1];
	Cell[BoxData[GridBox[table, ColumnAlignments -> Left]], CellTags->"CocoaCell"]
];


CocoaObjectDebugNotebook[] := NotebookPut[
	Notebook[{
		CellGroupData[{
			Cell[BoxData[GridBox[{{
				ButtonBox["Update",
					ButtonEvaluator->Automatic,
					ButtonFunction:>(Needs["CocoaLink`"]; 
						CocoaObjectDebugUpdateNotebook[ButtonNotebook[]]),
					Active->True
					],
				ButtonBox["Clear",
					ButtonEvaluator->Automatic,
					ButtonFunction:>(Needs["CocoaLink`"]; CocoaClear[]; 
						CocoaObjectDebugUpdateNotebook[ButtonNotebook[]]),
					Active->True
					]
				}}]]],
				CocoaObjectDebugCell[]
			}]
		},
		Editable->False,
		WindowFrame->"Palette",
		WindowClickSelect->False,
		WindowElements->{"HorizontalScrollBar", "VerticalScrollBar"},
		WindowSize->{ Automatic, 400 },
		ShowCellBracket->False
	]
];


ArgString[args_Integer] := Which[
	args === 0, "",
	args === 1, "\[Placeholder]",
	True, StringJoin @@ Append[Table["\[Placeholder],", { args - 1 }], "\[Placeholder]"]
];


CocoaObjectMethodCell[CocoaObject[obj_Integer]] := Module[
	{ meths = CocoaObjectMethods[CocoaObject[obj]], table },
	table = (
		ButtonBox["\[SelectionPlaceholder]@" <> ToString[#] <> "[" <> ArgString[Length[StringPosition[ToString[#], ":"]]] <> "]",
			ButtonData->"\[SelectionPlaceholder]@" <> ToString@CocoaSelector[#] <> "[" <> ArgString[Length[StringPosition[ToString[#], ":"]]] <> "]",
			ButtonStyle->"Paste",
			Active->True
		]& /@ meths
	);
	table = Insert[table, 
		StyleBox[CocoaObjectClass[CocoaObject[obj]], FontWeight -> "Bold", FontVariations -> {"Underline" -> True}], 
		1]; 
	Cell[BoxData[GridBox[Transpose[{table}], RowSpacings->{ 0, 0 }]], 
		CellTags->"CocoaCell"]
];


CocoaObjectMethodsNotebook[CocoaObject[obj_Integer]] := NotebookPut[
	Notebook[{ CocoaObjectMethodCell[CocoaObject[obj]] },
		Editable->False,
		WindowFrame->"Palette",
		WindowClickSelect->False,
		WindowElements->{ "VerticalScrollBar" },
		WindowSize->{ FitAll, 400 },
		ShowCellBracket->False
	]
];


CocoaClassesCell[] := Module[{ classes = CocoaClasses[], table },
	table = (
		{ButtonBox[#,
			ButtonEvaluator->Automatic,
			ButtonFunction:>(Needs["CocoaLink`"]; CocoaObjectMethodsNotebook[CocoaLoadClass[#]];),
			Active->True
		]}& /@ Sort[classes]
	);
	Cell[BoxData[GridBox[table, RowSpacings->{ 0, 0 }]], CellTags->"CocoaCell"]
];


CocoaClassesNotebook[] := NotebookPut[
	Notebook[{ CocoaClassesCell[] },
		Editable->False,
		WindowFrame->"Palette",
		WindowClickSelect->False,
		WindowElements->{ "VerticalScrollBar" },
		WindowSize->{ FitAll, 400 },
		ShowCellBracket->False
	]
];


End[];(*CocoaLink`Private`*)


(* ::Section:: *)
(*Finalization*)


EndPackage[];(*CocoaLink`*)
